/**
 * The CfgMngr implementation of the configuration interface, used to manage
 * subsystem configurations.
 */
package za.org.jcicada.cfgmngr.impl;

import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import za.org.jcicada.cfgmngr.IConfigurationClient;
import za.org.jcicada.cfgmngr.IConfigurationServer;
import za.org.jcicada.cfgmngr.IPhysicalServer;
import za.org.jcicada.cfgmngr.ISystemObserver;
import za.org.jcicada.cfgmngr.ISystemSubject;
import za.org.jcicada.cfgmngr.SystemSetType;
import za.org.jcicada.genss.ServiceURN;
import za.org.jcicada.genss.SystemURN;
import za.org.jcicada.genss.messages.Result;
import za.org.jcicada.genss.messages.SuccessCode;
import za.org.jcicada.genss.sensor.SensorURN;
import za.org.jcicada.genss.sensor.impl.InvalidSensorValue;
import za.org.jcicada.genss.sensor.impl.Sensor;
import za.org.jcicada.genss.sensor.impl.StatusStrategyFactory;
import za.org.jcicada.genss.sensor.impl.TypeInt;

/**
 * Initial implementation of IConfigurationServer. It stores the registered
 * services in a map and loads properties files using resource names when
 * requested to get a configuration.
 */
public class ConfigurationServer implements IConfigurationServer {

    /** Class logger */
    private static final Log logger = LogFactory.getLog(ConfigurationServer.class);

    /** The system subject is notified of system registration events */
    protected SystemSubject systemSubject = new SystemSubject();
    
    /** The set of systems that have been registered */
    protected Set<SystemURN> systems = new HashSet<SystemURN>();

    /** The map of services and a list of their providers */
    protected Map<ServiceURN, Set<URI>> services = new HashMap<ServiceURN, Set<URI>>();

    /** The map of sensors for each system */
    protected Map<SystemURN, Set<SensorURN>> systemSensors = new HashMap<SystemURN, Set<SensorURN>>();
    
    protected Set<SystemURN> fullSystemURNs = null;

    /** Handle to physical location server object */
    private PhysicalServer physicalServer;
    
    /** Map from system types to classes */
    private Properties typeClassMap;
    
    /** Sensor recording the number of registered systems */
    private Sensor systemCountSensor;
    
    /** Sensor recording the number of registered sensors */
    private Sensor sensorCountSensor;
    
    public ConfigurationServer(PhysicalServer physicalServer, Properties typeClassMap) {
    	this.physicalServer = physicalServer;
    	this.typeClassMap = typeClassMap;
    	systemCountSensor = new Sensor(TypeInt.getInstance(0, Integer.MAX_VALUE), "systemCount", "Number of registered systems", "systems");
    	sensorCountSensor = new Sensor(TypeInt.getInstance(0, Integer.MAX_VALUE), "sensorCount", "Number of registered sensors", "sensors");
    }
    
    public void initialize(CfgMngrDynamicConfiguration config) {
    	systemCountSensor.setStatusStrategy(StatusStrategyFactory.createStatusStrategy(systemCountSensor, config.systemCountStatusStrategy));
    	sensorCountSensor.setStatusStrategy(StatusStrategyFactory.createStatusStrategy(sensorCountSensor, config.sensorCountStatusStrategy));
    }

    /**
     * Stop the configuration server and clear its state
     */
    public void stop() {
        systems.clear();
        services.clear();
        systemSensors.clear();
    }
    
    public Sensor[] getSensors() {
    	return new Sensor[] { systemCountSensor, sensorCountSensor };
    }

    public ISystemSubject getSystemSubject() {
    	return systemSubject;
    }
    
    public IPhysicalServer getPhysicalServer() {
    	return physicalServer;
    }
    
    public void setFullSystemURNs(Set<SystemURN> fullSystemURNs) {
    	this.fullSystemURNs = new HashSet<SystemURN>(fullSystemURNs);
    }
    
    public void requestGetConfiguration(IConfigurationClient client, SystemURN systemURN) {
        String propertyName = systemURN.getSystemName() + ".dynamic";
        Properties properties = new Properties();
        Result result = new Result();
        try {
            properties = PropertyLoader.loadProperties(propertyName);
        } catch (IllegalArgumentException ex) {
            String message = "Unable to load configuration for " + propertyName;
            logger.error(message);
            result = new Result(SuccessCode.COMPLETE_FAILURE, message);
        } finally {
            if (systemURN.isNodeProxy()) {
            	List<String> pairs = new ArrayList<String>();
            	try {
	            	for (SystemURN peerSystemURN : physicalServer.getPeerSystemsOf(systemURN)) {
	            		String type = peerSystemURN.getType();
	            		if (!type.equals(CfgMngr.SYSTEM_NAME)) {
	            			String clazz = typeClassMap.getProperty(type);
	            			pairs.add(String.format("%s=%s", peerSystemURN.getSystemName(), clazz));
	            		}
	            	}
	            	String systemURNClassMap = StringUtils.join(pairs, ";");
	            	properties.put("systemURNClassMap", systemURNClassMap);
	            	if (result.getCode() == SuccessCode.COMPLETE_FAILURE) {
	            		result = new Result(SuccessCode.PARTIAL_SUCCESS, "");
	            	}
            	} catch (NullPointerException ex) {
            		String message = "Can't find peers for " + systemURN;
            		logger.error(message);
            		result = new Result(SuccessCode.COMPLETE_FAILURE, message);
            	}
            }
        	client.replyGetConfiguration(result, properties);
        }
    }

    public void requestRegisterSystem(IConfigurationClient client, SystemURN systemURN) {
        if (null == systemURN) {
            String message = "requestRegisterSystem reports null subsystemURI";
            logger.error(message);
            client.replyRegisterSystem(new Result(SuccessCode.COMPLETE_FAILURE, message), systemURN, false);
            return;
        }
        boolean reregister = !systems.contains(systemURN);
        if (systems.add(systemURN)) {
        	updateSystemCount();
        	logger.info(String.format("Register system %s", systemURN));
        	systemSubject.notifySystemEvent(systemURN, SystemSetType.REGISTERED, ISystemObserver.Event.REGISTERED);
        }
        client.replyRegisterSystem(new Result(), systemURN, reregister);
    }

    public void requestDeregisterSystem(IConfigurationClient client, SystemURN systemURN) {
    	logger.info(String.format("Deregister system %s", systemURN));
        if (null == systemURN) {
            String message = "deregisterSystem reports null subsystemURI";
            logger.error(message);
            client.replyDeregisterSystem(new Result(SuccessCode.COMPLETE_FAILURE, message), systemURN);
            return;
        }
        if (systems.remove(systemURN)) {
        	updateSystemCount();
            systemSubject.notifySystemEvent(systemURN, SystemSetType.REGISTERED, ISystemObserver.Event.DEREGISTERED);
            client.replyDeregisterSystem(new Result(), systemURN);
        } else {
            String message = "Trying to deregister unknown subsystemURI " + systemURN;
            logger.warn(message);
            client.replyDeregisterSystem(new Result(SuccessCode.PARTIAL_SUCCESS, message), systemURN);
        }
    }

    public void requestRegisterService(IConfigurationClient client, URI locationURI, ServiceURN serviceURN) {
        logger.info(String.format("Register service %s at %s", serviceURN, locationURI));
        if ((null == serviceURN) || (null == locationURI)) {
            String message = "requestRegisterServices reports null parameter. urlSubsystem: " + locationURI + ", uriService: " + serviceURN;
            logger.error(message);
            client.replyRegisterService(new Result(SuccessCode.COMPLETE_FAILURE, message), locationURI, serviceURN);
            return;
        }
        if (!services.containsKey(serviceURN)) {
            services.put(serviceURN, new HashSet<URI>());
        }
        services.get(serviceURN).add(locationURI);
        // if a sensor then add to systemSensors
        if (serviceURN.getService().equals(SensorURN.URN_SERVICE_SENSORS)) {
        	SystemURN systemURN = serviceURN.getSystemURN();
            if (!systemSensors.containsKey(systemURN)) {
                systemSensors.put(systemURN, new HashSet<SensorURN>());
                systemSubject.notifySystemEvent(systemURN, SystemSetType.VIRTUAL, ISystemObserver.Event.REGISTERED);
            }
            systemSensors.get(systemURN).add((SensorURN) serviceURN);
            updateSensorCount();
        }
        client.replyRegisterService(new Result(), locationURI, serviceURN);
    }

    public void requestDeregisterService(IConfigurationClient client, URI locationURI, ServiceURN serviceURN) {
        if (null == serviceURN) {
            String message = "deregisterServices reports null serviceURN";
            logger.error(message);
            client.replyDeregisterService(new Result(SuccessCode.COMPLETE_FAILURE, message), locationURI, serviceURN);
            return;
        }
        Result result = new Result();
        // check the set of locations for the service
        Set<URI> locationURIs = services.get(serviceURN);
        if (locationURIs == null) {
            String message = String.format("deregisterServices reports unknown serviceURN (%s).", serviceURN.toString());
            logger.error(message);
            client.replyDeregisterService(new Result(SuccessCode.COMPLETE_FAILURE, message), locationURI, serviceURN);
            return;
        }
        // check the particular location for the service
        if (locationURI != null) {
		    if (locationURIs.remove(locationURI) == false) {
		        String message = String.format("deregisterServices reports unknown location (%s) for serviceURN (%s).", locationURI, serviceURN);
		        logger.error(message);
		        client.replyDeregisterService(new Result(SuccessCode.COMPLETE_FAILURE, message), locationURI, serviceURN);
		        return;
		    }
        }
        // is the service completely gone?
        if (locationURIs.size() == 0 || locationURI == null) {
            logger.info(String.format("Deregister service %s at %s", serviceURN, locationURI));
            services.remove(serviceURN);
            // if a sensor then remove from systemSensors
            if (serviceURN.getService().equals(SensorURN.URN_SERVICE_SENSORS)) {
            	SystemURN systemURN = serviceURN.getSystemURN();
                systemSensors.get(systemURN).remove(serviceURN);
                if (systemSensors.get(systemURN).isEmpty()) {
                	systemSensors.remove(systemURN);
                    systemSubject.notifySystemEvent(serviceURN.getSystemURN(), SystemSetType.VIRTUAL, ISystemObserver.Event.DEREGISTERED);
                }
                updateSensorCount();
            }
        }
        client.replyDeregisterService(result, locationURI, serviceURN);
    }

    public void requestLocateService(IConfigurationClient client, ServiceURN serviceURN, String protocol) {
        if (null == serviceURN || null == protocol) {
            String message = "locateService received null parameter. serviceURN: " + serviceURN + ", protocol: " + protocol;
            logger.error(message);
            client.replyLocateService(new Result(SuccessCode.COMPLETE_FAILURE, message), serviceURN, null);
            return;
        }
        Set<URI> locationURIs = services.get(serviceURN);
        if (locationURIs == null) {
            client.replyLocateService(new Result(SuccessCode.COMPLETE_FAILURE, String.format("Unknown serviceURN: %s", serviceURN)), serviceURN, null);
            return;
        }
        for (URI locationURI : locationURIs) {
            if (locationURI.getScheme().equals(protocol)) {
                client.replyLocateService(new Result(), serviceURN, locationURI);
                return;
            }
        }
        client.replyLocateService(new Result(SuccessCode.COMPLETE_FAILURE, String.format("No protocol %s for: %s", protocol, serviceURN.toString())), serviceURN, null);
    }

    public void requestSystemSet(IConfigurationClient client, SystemSetType type) {
    	switch(type) {
    	case REGISTERED:
    		client.replySystemSet(new Result(), type, new HashSet<SystemURN>(systems));
    		break;
    	case ENABLED:
        	if (fullSystemURNs != null ) {
        		client.replySystemSet(new Result(), type, new HashSet<SystemURN>(fullSystemURNs));
        	} else {
        		client.replySystemSet(new Result(SuccessCode.COMPLETE_FAILURE, "Full system set not configured"), type, null);
        	}
        	break;
    	case VIRTUAL:
    		client.replySystemSet(new Result(SuccessCode.COMPLETE_FAILURE, "Not implemented"), type, null);
    		break;
    	}
    }
    
    public void requestSensorSet(IConfigurationClient client, SystemURN systemURN) {
        if (null == systemURN) {
            String message = "requestSensorSet received null systemURN";
            logger.error(message);
            client.replySensorSet(new Result(SuccessCode.COMPLETE_FAILURE, message), systemURN, null);
            return;
        }
        Set<SensorURN> sensorURNs = systemSensors.get(systemURN);
        if (sensorURNs == null) {
            String message = String.format("Unrecognized systemURN: %s", systemURN);
            logger.warn(message);
            client.replySensorSet(new Result(SuccessCode.COMPLETE_FAILURE, message), systemURN, sensorURNs);
            return;
        }
        client.replySensorSet(new Result(), systemURN, systemSensors.get(systemURN));
    }
    
    public void requestServiceSet(IConfigurationClient client, SystemURN systemURN) {
    	if (null == systemURN) {
    		String message = "requestServiceSet received null systemURN";
    		logger.error(message);
    		client.replyServiceSet(new Result(SuccessCode.COMPLETE_FAILURE, message), systemURN, null);
    		return;
    	}
    	Set<ServiceURN> serviceURNs = new HashSet<ServiceURN>();
    	for (ServiceURN serviceURN : services.keySet()) {
    		if (serviceURN.getService().equals(ServiceURN.URN_SERVICE) && serviceURN.getSystemURN().equals(systemURN)) {
    			serviceURNs.add(serviceURN);
    		}
    	}
    	client.replyServiceSet(new Result(), systemURN, serviceURNs);
    }

    public void informDisconnect(IConfigurationClient client) {
        // Do nothing
    }

	private void updateSystemCount() {
		try {
			systemCountSensor.setValue(systems.size());
		} catch (InvalidSensorValue e) {
			logger.error(e);
		}
	}

	private void updateSensorCount() {
		int sensorCount = 0;
		for (Set<SensorURN> set : systemSensors.values()) {
			sensorCount += set.size();
		}
		try {
			sensorCountSensor.setValue(sensorCount);
		} catch (InvalidSensorValue e) {
			logger.error(e);
		}
	}

}
